<?php


namespace app\service;

use Elasticsearch\ClientBuilder;
use Elasticsearch\Common\Exceptions\BadRequest400Exception;
use think\db\Where;

/**
 * ElasticSearch搜索引擎服务 -- composer require elasticsearch/elasticsearch
 *  # ====================== Elasticsearch数据类型及其属性 =============
 *  核心类型    字符串类型    string,text,keyword
 *    整数类型    integer,long,short,byte
 *    浮点类型    double,float,half_float,scaled_float
 *    逻辑类型    boolean
 *    日期类型    date
 *    范围类型    range
 *    二进制类型    binary
 *  复合类型    数组类型    array
 *    对象类型    object
 *    嵌套类型    nested
 *  地理类型    地理坐标类型    geo_point
 *    地理地图    geo_shape
 *  特殊类型    IP类型    ip
 *    范围类型    completion
 *    令牌计数类型    token_count
 *    附件类型    attachment
 *    抽取类型    percolator
 *
 *  # ==================Mapping 支持属性 ======================
 *  enabled：仅存储、不做搜索和聚合分析
 *  index：是否构建倒排索引（即是否分词，设置false，字段将不会被索引）
 *  index_option：存储倒排索引的哪些信息 4个可选参数：docs：索引文档号  freqs：文档号+词频  positions：文档号+词频+位置，通常用来距离查询 offsets：文档号+词频+位置+偏移量，通常被使用在高亮字段  分词字段默认是positions，其他默认时docs
 *  norms：是否归一化相关参数、如果字段仅用于过滤和聚合分析、可关闭分词字段默认配置，不分词字段：默认{“enable”: false}，存储长度因子和索引时boost，建议对需要参加评分字段使用，会额外增加内存消耗
 *  doc_value：是否开启doc_value，用户聚合和排序分析对not_analyzed字段，默认都是开启，分词字段不能使用，对排序和聚合能提升较大性能，节约内存
 *  fielddata：是否为text类型启动fielddata，实现排序和聚合分析针对分词字段，参与排序或聚合时能提高性能，不分词字段统一建议使用doc_value
 *  store：是否单独设置此字段的是否存储而从_source字段中分离，只能搜索，不能获取值
 *  coerce：是否开启自动数据类型转换功能，比如：字符串转数字，浮点转整型
 *  multifields：灵活使用多字段解决多样的业务需求
 *  dynamic：控制mapping的自动更新
 *  data_detection：是否自动识别日期类型
 *  analyzer：指定分词器，默认分词器为standard analyzer
 *  boost：字段级别的分数加权，默认值是1.0
 *  fields：可以对一个字段提供多种索引模式，同一个字段的值，一个分词，一个不分词
 *  ignore_above：超过100个字符的文本，将会被忽略，不被索引
 *  include_in_all：设置是否此字段包含在_all字段中，默认时true，除非index设置成no
 *  null_value：设置一些缺失字段的初始化，只有string可以使用，分词字段的null值也会被分词
 *  position_increament_gap：影响距离查询或近似查询，可以设置在多值字段的数据上或分词字段上，查询时可以指定slop间隔，默认值时100
 *  search_analyzer：设置搜索时的分词器，默认跟analyzer是一致的，比如index时用standard+ngram，搜索时用standard用来完成自动提示功能
 *  similarity：默认时TF/IDF算法，指定一个字段评分策略，仅仅对字符串型和分词类型有效
 *  trem_vector：默认不存储向量信息，支持参数yes（term存储），with_positions（term+位置），with_offsets（term+偏移量），with_positions_offsets（term+位置+偏移量）对快速高亮fast vector highlighter能提升性能，但开启又会加大索引体积，不适合大数据量用
 * Class ElasticSearchService
 * @package App\Service
 */
class ElasticSearchService
{
    private static $ElasticSearchObj = null;
    //ElasticSearch版本
    private static $version = 7.9;

    /**
     * elasticsearch-7.9.3-windows-x86_64\elasticsearch-7.9.3\config 里面的elasticsearch.yml后面添加 重启elasticsearch服务
     * http.cors.enabled: true
     * http.cors.allow-origin: "*"
     * 启动elasticsearch-head-master才会成功
     * ElasticSearchService constructor.
     */
    private function __construct()
    {
    }

    static public function getInstance()
    {
        if (empty(self::$ElasticSearchObj)) {
            $params = array(
                env('ELASTIC_HOST', '127.0.0.1:9200')
            );
            //->setHosts($params)
            self::$ElasticSearchObj = ClientBuilder::create()->build();
        }
        return self::$ElasticSearchObj;
    }

    /**
     * 精确查找
     * 案例 : $ccc = ElasticSearchService::searchTerm('first_name','abc','hahahe','hahah2');
     * @param $field
     * @param $value
     * @param string $index_name
     * @param string $index_type
     * @return array
     */
    static public function searchTerm($field, $value, $select="*", $index_name = 'data_test', $index_type = 'index_type', $from = 0, $size = 2)
    {
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => [
                    'constant_score' => [ //非评分模式执行
                        'filter' => [     //过滤器，不会计算相关度，速度快
                            'term' => [   //精确查找，不支持多个条件
                                $field => $value
                            ]
                        ]
                    ]
                ],
                'size' => $size,
                'from' => $from,
                '_source' => $select,
            ]
        ];
        if (self::$version < 8) {
            $params['type'] = $index_type;
        }
        $results = self::getInstance()->search($params);
        //获取到的数据
        $data_arr = [];
        $data = $results['hits']['hits'];
        if($data){
            foreach ($data as $key=>$value){
                $data_arr['list'][] = $value['_source'];
            }
            $data_arr['total'] = $results['hits']['total']['value'];
        }
        return $data_arr;
    }

    /**
     * 模糊查找
     * 案例 : $ccc = ElasticSearchService::searchLike('first_name','abc',['first_name','last_name','id','age'],'hahahe','hahah2',0,10);
     * @param $field
     * @param $value
     * @param $select
     * @param string $index
     * @param string $index_type
     * @param int $from
     * @param int $size
     * @return array
     */
    static public function searchLike($field, $value, $select="*", $index = 'data_test', $index_type = '_doc', $from = 0, $size = 2)
    {
        $params = [
            'index' => $index,
            'body' => [
                'query' => [
                    'bool' => [    //非评分模式执行
                        'should' => [
                            'match' => [
                                $field => '*' . $value . '*',
                            ],

                        ],
//                        'filter' => [
//                            'term' => [
//                                $field => '*' . $value . '*',
//                                ],
//                        ],
                    ]
                ],
                'size' => $size,
                'from' => $from,
                '_source' => $select, // 这个就是select的意思后面加需要的字段
                // "aggs"=> [//聚合函数  https://blog.csdn.net/R_P_J/article/details/78376622  group by  这group by 应该再做一个方法才行
                //       "group_by_last_name"=>[//该名称可以随便起
                //              "terms"=>[ "field"=>'age' ]
                //       ]
                // ]
            ],
        ];
        if (self::$version < 8) {
            $params['type'] = $index_type;
        }
        $results = self::getInstance()->search($params);
        //获取到的数据
        $data_arr = [];
        $data = $results['hits']['hits'];
        if($data){
            foreach ($data as $key=>$value){
                $data_arr['list'][] = $value['_source'];
            }
            $data_arr['total'] = $results['hits']['total']['value'];
        }
        return $data_arr;
    }

    /**
     * 查询文档 (分页，排序，权重，过滤)
     * 实例:  $searchLikedoc: [['key'=>'first_key','val'=>'拉','boost'=>3],['key'=>'first_key','val'=>'拉','boost'=>3]]
     * boost 表示权重
     * 实例: $sort  ['age'=>['order'=>'desc']]
     * 表是 age字段 用倒序排序
     * 如： ElasticSearchService::searchLikedoc([['key'=>'first_key','val'=>'拉','boost'=>3],['key'=>'first_key','val'=>'拉','boost'=>3]],['age'=>['order'=>'desc']],'hahahe','hahah2');
     * @param array $searchLikedoc
     * @param array $sort
     * @param string $index_name
     * @param string $index_type
     * @param int $from
     * @param int $size
     * @param string $select
     * @return array
     */
    static public function searchLikedoc($searchLikedoc = [], $sort = [], $index_name = "test_ik", $index_type = "goods", $from = 0, $size = 20,$select="*")
    {

        $findArr = [];
        foreach ($searchLikedoc as $key => $value) {
            $findArr [] = ['match' => [$value['key'] => ['query' => $value['val'], 'boost' => empty($value['boost']) ? 3 : $value['boost']]]];
        }
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => [
                    'bool' => [
                        'should' => $findArr
                    ],
                ],
                'sort' => $sort // ['age'=>['order'=>'desc']]
                ,
                'from' => $from,
                'size' => $size,
                '_source' => $select,
            ]
        ];
        if (self::$version < 8) {
            $params['type'] = $index_type;
        }
        $results = self::getInstance()->search($params);
        //获取到的数据
        $data_arr = [];
        $data = $results['hits']['hits'];
        if($data){
            foreach ($data as $key=>$value){
                $data_arr['list'][] = $value['_source'];
            }
            $data_arr['total'] = $results['hits']['total']['value'];
        }
        return $data_arr;
    }


    /**
     * 高亮搜索
     * ElasticSearchService::highlight_search(['goods_name','keywords'],'马卡','bsl_goods','bsl_goods',['id','goods_name','keywords']);
     * @param $field
     * @param $value
     * @param string $index_name
     * @param string $index_type
     * @param string $select
     * @param array $sort
     * @param int $from
     * @param int $size
     * @return array
     */
    public static function highlight_search($field, $value, $index_name = 'data_test', $index_type = 'index_type', $select="*",$sort=[],$from = 0, $size = 20)
    {
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => [
                    'multi_match' => [
                        "query"=> $value,
                        "fields"=> $field,
                    ]
                ],
                'highlight' => [
                    'fields' => [
                        '*' => new \stdClass(),
                    ],
                    "pre_tags"=> ["<span style='color:red'>"],
                    "post_tags"=>["</span>"],
                ],
                'sort' => $sort // ['age'=>['order'=>'desc']]
                ,
                'from' => $from,
                'size' => $size,
                '_source' => $select,
            ]
        ];
        if (self::$version < 8) {
            $params['type'] = $index_type;
        }
        $res = self::getInstance()->search($params);
        //获取到的数据
        $data_arr = [];
        $data = $res['hits']['hits'];
        if($data){
            foreach ($data as $key=>$value){
                $value['_source']['highlight'] = $value['highlight'];
                $data_arr['list'][] = $value['_source'];
                //$products_name= $value['highlight']['products_name'];
            }
            $data_arr['total'] = $res['hits']['total']['value'];
        }
        return $data_arr;
    }

    /**
     * 创建索引  就是创建库 只能创建一次
     * 案例 : $ccc = ElasticSearchService::create_index('hahahe');
     * @param string $index_name 索引名--相当于mysql数据库名
     * @return array|mixed|string
     */
    static public function create_index($index_name = 'test_ik')
    {
        // 只能创建一次
        $params = [
            'index' => $index_name,
            'body' => [
                'settings' => [
                    'number_of_shards' => 5,
                    'number_of_replicas' => 0
                ]
            ]
        ];
        try {
            return self::getInstance()->indices()->create($params);
        } catch (BadRequest400Exception $e) {
            $msg = $e->getMessage();
            $msg = json_decode($msg, true);
            return $msg;
        }
    }

    /**
     * 删除索引
     * 实例: ElasticSearchService::delete_index('hahahe');
     * @param string $index_name 索引名--相当于mysql数据库名
     * @return array
     */
    static public function delete_index($index_name = 'test_ik')
    {
        $params = ['index' => $index_name];
        $response = self::getInstance()->indices()->delete($params);
        return $response;
    }

    /**
     * 创建文档模板 就是索引格式,
     * 就是创建 表  只用创建一次 目前没有修改功能
     * $ccccc   =  ['id' => ['type' => 'integer'],'first_name' => ['type' => 'text','analyzer' => 'ik_max_word' //这个是中文分词],'last_name' => ['type' => 'text','analyzer' => 'ik_max_word'],'age' => ['type' => 'integer']];
     * $ccc = ElasticSearchService::create_mappings('hahahe','hahah2',$ccccc);
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @param array $properties
     * @return array
     */
    static public function create_mappings($index_name = 'test_ik', $type_name = 'goods', $properties = [])
    {
        $params = [
            'include_type_name' => true,
            'index' => $index_name,
            'body' => [
                $type_name => [
                    '_source' => [
                        'enabled' => true
                    ],
                    'properties' => $properties
                ]
            ]
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        return self::getInstance()->indices()->putMapping($params);
    }

    /**
     * 查看映射 就是  表的 文档模板
     * 实例: $ccc = ElasticSearchService::get_mapping(hahahe','hahah2');
     * @param string $type_name 文档名 -- 相当于mysql数据表名 -- ElasticSearch 7.X开始不支持索引类型--ElasticSearch 8.X不在有索引类型
     * @param string $index_name 索引名--相当于mysql数据库名
     * @return array
     */
    static public function get_mapping($type_name = 'goods', $index_name = 'test_ik')
    {
        $params = [
            'index' => $index_name,
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
            $params['include_type_name'] = true; //在ElasticSearch 8.X版本type不在支持-- 小于6.X版本的支持多个type ---7.X只支持一个type类型
        }
        $response = self::getInstance()->indices()->getMapping($params);
        return $response;
    }


    /**
     * 添加文档 --就是添加每一行的数据
     * 实例 : $ccc = ElasticSearchService::add_doc(6,['id'=>123,'first_name'=>'abc','last_name'=>'abc','age'=>243], 'hahahe','hahah2');
     * @param integer $id --主键id---页可以在字段类型type设置的时候integer-页可以自定义
     * @param $doc --需要添加的内容
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return array
     */
    static public function add_doc($id, $doc, $index_name = 'test_ik', $type_name = 'goods')
    {
        $params = [
            'index' => $index_name,
            'id' => $id,
            'body' => $doc
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        $response = self::getInstance()->index($params);
        return $response;
    }

    /**
     * 判断文档存在
     * 实例 : ElasticSearchService::exists_doc(3,'hahahe','hahah2');
     * @param int $id --主键id
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return bool
     */
    static public function exists_doc($id = 1, $index_name = 'test_ik', $type_name = 'goods')
    {
        $params = [
            'index' => $index_name,
            'id' => $id
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        $response = self::getInstance()->exists($params);
        return $response;
    }


    /**
     * 获取文档
     * 实例: $ccc = ElasticSearchService::get_doc(2,'hahahe','hahah2');
     * @param string $id --主键id
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @param array $includes 字段显示
     * @param integer $from 页码值
     * @param integer $size 每页个数
     * @return array
     */
    static public function get_doc($id = '', $index_name = 'test_ik', $type_name = 'goods',$includes=[], $from = 0, $size = 20)
    {
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        if (empty($id)) {
            if($includes){
                $params = [
                    'index' => $index_name,
                    'body' => [
                        "_source" => [
                            "includes" => $includes,
                        ],
                        'from' => $from,
                        'size' => $size
                    ]
                ];
            }else{
                $params = [
                    'index' => $index_name,
                    'body' => [
                        'from' => $from,
                        'size' => $size
                    ]
                ];
            }

            $res = self::getInstance()->search($params);
            //获取到的数据
            $data_arr = [];
            $data = $res['hits']['hits'];
            if($data){
                foreach ($data as $key=>$value){
                    $data_arr[] = $value['_source'];
                }
            }
            return $data_arr;
        } else {
            $params = [
                'index' => $index_name,
                'id' => $id,
            ];
            $response = self::getInstance()->get($params);
            if($response){
                $response = $response['_source'];
            }
            return $response;
        }

    }

    /**
     * 更新文档
     * 实例 :  $ccc = ElasticSearchService::update_doc(2,['id'=>123,'first_name'=>'是发送到发鬼地方二而asdfasfd7.8dsser','last_name'=>'是发送到发鬼地方二而asdfasfd7.8dsser','age'=>23],'hahahe','hahah2');
     * @param int $id
     * @param $doc --需要跟新的内容
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return array
     */
    static public function update_doc($id = 1, $doc, $index_name = 'test_ik', $type_name = 'goods')
    {
        // 可以灵活添加新字段,最好不要乱添加
        $params = [
            'index' => $index_name,
            'id' => $id,
            'body' => ['doc' => $doc]
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        $response = self::getInstance()->update($params);
        return $response;
    }

    /**
     * 删除文档
     * 实例: $ccc = ElasticSearchService::delete_doc(2, 'hahahe','hahah2');
     * @param int $id
     * @param string $index_name 索引名--相当于mysql数据库名
     * @param string $type_name 文档名 -- 相当于mysql数据表名
     * @return array
     */
    static public function delete_doc($id = 1, $index_name = 'test_ik', $type_name = 'goods')
    {

        $params = [
            'index' => $index_name,
            'id' => $id
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        $response = self::getInstance()->delete($params);
        return $response;
    }


    /**
     * 判断指定的索引名是否存在
     * @param $index_name -索引名--相当于mysql数据库名
     * @return bool
     */
    static public function isExistsIndex($index_name)
    {
        $params = [
            'index' => $index_name,
        ];
        $response = self::getInstance()->indices()->exists($params);
        return $response;
    }

    /**
     * 判断指定的索引的类型是否存在
     * @param $index_name -索引名--相当于mysql数据库名
     * @param $type_name -索引名--相当于mysql数据库名
     * @return bool
     */
    static public function isExistsType($index_name, $type_name)
    {
        $params = [
            'index' => $index_name,
            'type' => $type_name,
        ];
        $response = self::getInstance()->indices()->existsType($params);
        return $response;
    }


    /**
     * 删除内容
     * @param $index_name
     * @param $type_name
     * @param array $query
     */
    static public function deleteByQuery($index_name, $type_name, $query = [])
    {
        $params = [
            'index' => $index_name,
            'body' => [
                'query' => $query
            ],
        ];
        if (self::$version < 8) {
            $params['type'] = $type_name;
        }
        self::getInstance()->deleteByQuery($params);
    }

    /**
     * 根据模型的表结构创建索引和索引类型
     * @param $dataModel -框架模型--models下面的
     * @param string $index_name -索引名--相当于mysql数据库名
     * @param string $type_name -索引类型--相当于mysql数据表名
     * @param bool $add_data -是否添加数据
     * @return bool|mixed|string
     */
    static public function createLibraryTable($dataModel, $index_name = "kg_server", $type_name = "bsl_server",$add_data=false)
    {
        try {
            //判断索引是否存在
            $is_elastic_index = self::isExistsIndex($index_name);
            if (!$is_elastic_index) {
                //创建索引
                self::create_index($index_name);
            }
            //判断索引类型是否存在
            $is_elastic_type = self::isExistsType($index_name, $type_name);
            if (!$is_elastic_type) {
                //获取表字段和类型
                $properties = [];
                $getTableColumns = $dataModel->getTableColumns();
                //创建索引类型 -- 'analyzer' => 'ik_max_word'--如果没有安装用就会报错
                foreach ($getTableColumns as $key => $value) {
                    //这个getTableColumnsType函数找不到,要报错
                    //$getTableColumnsType = $dataModel->getTableColumnsType($value);
                    if ($value != "id") {
                        $getTableColumnsType = "text";
                    }else{
                        $getTableColumnsType = "integer";
                    }
                    $properties[$value] = ['type' => $getTableColumnsType];
                }
                self::create_mappings($index_name, $type_name, $properties);
            }
            if($add_data){
                $data = $dataModel->get()->toArray();
                //添加数据
                foreach ($data as $_key => $_value) {
                    $exits_doc = ElasticSearchService::exists_doc($_value['id'], $index_name, $type_name);
                    if (!$exits_doc) {
                        ElasticSearchService::add_doc($_value['id'], $_value, $index_name, $type_name);
                    }
                }
            }
            return true;
        } catch (BadRequest400Exception $e) {
            $msg = $e->getMessage();
            $msg = json_decode($msg, true);
            return $msg;
        }
    }

    /**
     * 索引数据统计和显示索引信息
     * @param $params --实例:$params['index'] = 'my_index';
     */
    static public function get_indices($params)
    {
        self::getInstance()->indices()->stats($params);
    }

    /**
     * 节点数据统计和显示节点信息
     */
    static public function get_nodes()
    {
        self::getInstance()->nodes()->stats();
    }

    /**
     * 集群数据统计和显示集群信息
     */
    static public function get_cluster()
    {
        self::getInstance()->cluster()->stats();
    }

    static public function get_snapshot()
    {
        self::getInstance()->snapshot();
    }
}
